<?php

namespace DreamCat\Array2Class;

use DreamCat\Array2Class\Annotation\ObjectAttribute;
use DreamCat\Array2Class\Exception\ConvertFailed;
use DreamCat\Array2Class\Utils\FeatureJudge;
use Dreamcat\PropertyAnalysis\Annotation\ArrayDescAnno;
use Dreamcat\PropertyAnalysis\Pojo\BuildinType;
use Dreamcat\PropertyAnalysis\Pojo\OneMayTypeDesc;
use Dreamcat\PropertyAnalysis\Pojo\PropertyResult;
use Dreamcat\PropertyAnalysis\Pojo\TypeDescription;
use Dreamcat\PropertyAnalysis\PropertyAnalysis;
use Dreamcat\PropertyAnalysis\PropertyAnalysisInterface;
use Dreamcat\PropertyAnalysis\Utils\BuildinTypeHelper;
use JetBrains\PhpStorm\Pure;
use MyCLabs\Enum\Enum;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionEnum;
use ReflectionNamedType;
use ReflectionProperty;
use ReflectionType;
use ReflectionUnionType;
use stdClass;
use Throwable;

/**
 * 数组转类对象的转换器
 * @author vijay
 */
class Array2ClassConverter implements Array2ClassInterface
{
    /** @var bool 是否创建对象原本不存在的属性 */
    private bool $createNewProperty = false;
    /** @var bool 是否尝试执行不调用构造函数创建对象 */
    private bool $tryWithoutConstructor = false;
    /** @var bool 是否尝试调用 setter */
    private bool $tryCallSetter = true;
    /** @var bool 尝试在构造函数只有一个参数时调用构造函数 */
    private bool $tryConstructWithParam = true;

    /** @var PropertyAnalysisInterface 类的属性分析器 */
    private PropertyAnalysisInterface $propertyAnalysis;

    /** @var ?PropertyNameTransInterface 自动名称转换接口 */
    private ?PropertyNameTransInterface $propertyNameTrans = null;

    /**
     * @return PropertyNameTransInterface|null
     */
    #[Pure]
    public function getPropertyNameTrans(): ?PropertyNameTransInterface
    {
        return $this->propertyNameTrans;
    }

    /**
     * @param PropertyNameTransInterface|null $propertyNameTrans
     */
    public function setPropertyNameTrans(?PropertyNameTransInterface $propertyNameTrans): void
    {
        $this->propertyNameTrans = $propertyNameTrans;
    }

    /**
     * @return bool 尝试在构造函数只有一个参数时调用构造函数
     */
    #[Pure]
    public function isTryConstructWithParam(): bool
    {
        return $this->tryConstructWithParam;
    }

    /**
     * @param bool $tryConstructWithParam 尝试在构造函数只有一个参数时调用构造函数
     */
    public function setTryConstructWithParam(bool $tryConstructWithParam): void
    {
        $this->tryConstructWithParam = $tryConstructWithParam;
    }

    /**
     * @return bool 是否创建对象原本不存在的属性
     */
    #[Pure]
    public function isCreateNewProperty(): bool
    {
        return $this->createNewProperty;
    }

    /**
     * 是否创建对象原本不存在的属性
     * @param bool $createNewProperty 是否创建对象原本不存在的属性
     * @note 设置为true之后如果数组的值是对象不存在的属性则为其创建同名新属性
     */
    public function setCreateNewProperty(bool $createNewProperty): void
    {
        $this->createNewProperty = $createNewProperty;
    }

    /**
     * @return bool 是否尝试执行不调用构造函数创建对象
     */
    #[Pure]
    public function isTryWithoutConstructor(): bool
    {
        return $this->tryWithoutConstructor;
    }

    /**
     * @param bool $tryWithoutConstructor 是否尝试执行不调用构造函数创建对象
     * @note 如果构建函数不满足条件，会直接构造对象，并不调用构造函数
     */
    public function setTryWithoutConstructor(bool $tryWithoutConstructor): void
    {
        $this->tryWithoutConstructor = $tryWithoutConstructor;
    }

    /**
     * @return bool 是否尝试调用 setter
     */
    #[Pure]
    public function isTryCallSetter(): bool
    {
        return $this->tryCallSetter;
    }

    /**
     * @param bool $tryCallSetter 是否尝试调用 setter
     * @note 设置为false或找不到setter会直接修改属性值
     */
    public function setTryCallSetter(bool $tryCallSetter): void
    {
        $this->tryCallSetter = $tryCallSetter;
    }

    /** @inheritDoc */
    public function convert(mixed $data, string $typeName): mixed
    {
        if (class_exists($typeName)) {
            return $this->convertByRef($data, new ReflectionClass($typeName));
        } elseif (isset(BuildinTypeHelper::IS_SCALAR[$typeName])) {
            $one = $this->getTypeDescByName($typeName);
            if ($one) {
                $type = new TypeDescription();
                $type->addTypeDescription($one);
                return $this->createValue($data, $type);
            }
        }
        throw new ConvertFailed("指定的输出类型 {$typeName} 不存在");
    }

    /**
     * @inheritDoc
     * @noinspection PhpDocMissingThrowsInspection
     */
    public function convertByRef(mixed $data, ReflectionClass $reflectionClass): object
    {
        return $this->convertByRefAllowNull($data, $reflectionClass);
    }

    /**
     * 转为内置枚举
     * @param mixed $data 数据
     * @param string $className 枚举类名
     * @return object|null
     */
    protected function convert2InnerEnum(mixed $data, string $className): ?object
    {
        if (!enum_exists($className)) {
            // @codeCoverageIgnoreStart
            throw new ConvertFailed("{$className} 非枚举类");
            // @codeCoverageIgnoreEnd
        }
        $ref = new ReflectionEnum($className);
        /** @var ?ReflectionNamedType $backType */
        $backType = $ref->getBackingType();
        try {
            if ($ref->isBacked() && $backType) {
                $data = match ($backType->getName()) {
                    BuildinType::INT => intval($data),
                    default => strval($data),
                };
                return call_user_func(
                    [
                        $className,
                        "from",
                    ],
                    $data
                );
            } else {
                return $ref->getCase(strval($data))->getValue();
            }
        } catch (Throwable $e) {
            throw new ConvertFailed("{$className} 不支持输入的值 {$data}", $e->getCode(), $e);
        }
    }

    /**
     * 通过反射器转换，可能返回null值
     * @param mixed $data 要转换的数据
     * @param ReflectionClass $reflectionClass 目标类的反射
     * @return object|null 转换后的对象或null
     * @noinspection PhpDocMissingThrowsInspection
     */
    protected function convertByRefAllowNull(mixed $data, ReflectionClass $reflectionClass): ?object
    {
        if (is_array($data) && $reflectionClass->getName() == stdClass::class) {
            return (object)$data;
        } elseif (is_object($data) && get_class($data) == $reflectionClass->getName()) {
            return $data;
        }
        if ($reflectionClass->isSubclassOf(Enum::class)) {
            # 是枚举类型
            return $this->convert2Enum($data, $reflectionClass);
        } elseif (FeatureJudge::hasEnum() && $reflectionClass->isEnum()) {
            return $this->convert2InnerEnum($data, $reflectionClass->getName());
        }

        if (!is_array($data)) {
            throw new ConvertFailed("{$reflectionClass->name}无法使用非数组构建");
        }

        # 尝试根据反射信息创建对象
        if ($reflectionClass->implementsInterface(PlainOBjectFactory::class)) {
            # 有实现简单对象工厂，直接调用
            /** @noinspection PhpUnhandledExceptionInspection */
            $object = $reflectionClass->getMethod("createPlainOBjectFactory")->invoke(null);
            $className = get_class($object);
            if ($className != $reflectionClass->getName()) {
                /** @noinspection PhpUnhandledExceptionInspection */
                $reflectionClass = new ReflectionClass($object);
            }
        } else {
            # 通过构造函数创建
            $constructor = $reflectionClass->getConstructor();
            if (!$constructor || $constructor->getNumberOfRequiredParameters() == 0) {
                # 没有必填参数，比较开心，直接创建
                /** @noinspection PhpUnhandledExceptionInspection */
                $object = $reflectionClass->newInstance();
            } elseif (
                $this->tryConstructWithParam
                && $constructor->getNumberOfParameters() > 0
                && $constructor->getNumberOfRequiredParameters() <= 1
            ) {
                /** @noinspection PhpUnhandledExceptionInspection */
                return $reflectionClass->newInstance($data);
            } elseif ($this->tryWithoutConstructor) {
                /** @noinspection PhpUnhandledExceptionInspection */
                $object = $reflectionClass->newInstanceWithoutConstructor();
            } else {
                throw new ConvertFailed(
                    "指定的输出类{$reflectionClass->name}构造函数需要参数且未实现接口" . PlainOBjectFactory::class
                );
            }
        }

        return $this->assignClassProperty($data, $object, $reflectionClass);
    }

    /** @noinspection PhpDocMissingThrowsInspection */

    /**
     * 处理枚举的转换
     * @param mixed $data 要处理的数据
     * @param ReflectionClass $reflectionClass 枚举反射类
     * @return ?Enum
     */
    protected function convert2Enum(mixed $data, ReflectionClass $reflectionClass): ?Enum
    {
        /** @noinspection PhpUnhandledExceptionInspection */
        if ($reflectionClass->getMethod("isValid")->invoke(null, $data)) {
            /** @noinspection PhpUnhandledExceptionInspection */
            /** @noinspection PhpIncompatibleReturnTypeInspection */
            return $reflectionClass->newInstanceArgs([$data]);
        }
        /** @noinspection PhpUnhandledExceptionInspection */
        $values = $reflectionClass->getMethod("toArray")->invoke(null);
        $findRet = array_search($data, $values);
        if ($findRet === false) {
            throw new ConvertFailed("{$reflectionClass->name}是枚举类型而输入数据不合法");
        }
        /** @noinspection PhpUnhandledExceptionInspection */
        /** @noinspection PhpIncompatibleReturnTypeInspection */
        return $reflectionClass->newInstanceArgs([$values[$findRet]]);
    }

    /**
     * 根据数据赋值对象属性
     * @param array $data 源数据
     * @param object $object 要赋值的对象
     * @param ReflectionClass $reflectionClass $object的反射
     * @return object 赋值后的对象
     * @noinspection PhpDocMissingThrowsInspection
     */
    protected function assignClassProperty(array $data, object $object, ReflectionClass $reflectionClass): object
    {
        $analysisResult = $this->getAnalysisResult($reflectionClass);
        foreach ($data as $key => $value) {
            if (isset($analysisResult[$key])) {
                # 存在相应属性
                $propertyResult = $analysisResult[$key];
                $property = $propertyResult->getProperty();
                if ($this->propertyIsIgnore($property)) {
                    continue;
                }
                $this->setPropertyValue($propertyResult, $value, $object);
            } elseif ($this->createNewProperty) {
                # 不存在，直接创建属性并赋值
                /** @noinspection PhpVariableVariableInspection */
                $object->$key = $value;
            }
        }

        # 判断非空项是否均已赋值
        foreach ($analysisResult as $propertyResult) {
            $property = $propertyResult->getProperty();
            if ($this->propertyIsIgnore($property)) {
                continue;
            }
            if (!$propertyResult->getTypeDescription() || $propertyResult->getTypeDescription()->isAllowNull()) {
                # 可以为null，忽略
                continue;
            }

            $allow = $property->isPublic();
            if (!$allow && FeatureJudge::needSetAccessible()) {
                // @codeCoverageIgnoreStart
                /** @noinspection PhpExpressionResultUnusedInspection */
                $property->setAccessible(true);
                // @codeCoverageIgnoreEnd
            }
            if ($property->isInitialized($object)) {
                $currentValue = $property->getValue($object);
            } else {
                $currentValue = null;
            }
            if (!$allow && FeatureJudge::needSetAccessible()) {
                // @codeCoverageIgnoreStart
                /** @noinspection PhpExpressionResultUnusedInspection */
                $property->setAccessible(false);
                // @codeCoverageIgnoreEnd
            }
            if ($currentValue === null) {
                throw new ConvertFailed("{$reflectionClass->getName()}::{$property->name}是必填属性但是未赋值");
            }
        }
        return $object;
    }

    /**
     * 获取类的分析结果
     * @param ReflectionClass $reflectionClass 类反射
     * @return PropertyResult[]
     */
    protected function getAnalysisResult(ReflectionClass $reflectionClass): array
    {
        $analysisResult = $this->getPropertyAnalysis()->analysis($reflectionClass)->getPropertyList();
        $result = [];
        foreach ($analysisResult as $propertyResult) {
            $name = $this->getPropertyName($propertyResult->getProperty());
            if (!isset($result[$name])) {
                $result[$name] = $propertyResult;
            }
        }
        return $result;
    }

    /**
     * @return PropertyAnalysisInterface 类的属性分析器
     */
    public function getPropertyAnalysis(): PropertyAnalysisInterface
    {
        if (!isset($this->propertyAnalysis)) {
            $this->setPropertyAnalysis(new PropertyAnalysis());
        }
        return $this->propertyAnalysis;
    }

    /**
     * @param PropertyAnalysisInterface $propertyAnalysis 类的属性分析器
     */
    public function setPropertyAnalysis(PropertyAnalysisInterface $propertyAnalysis): void
    {
        $this->propertyAnalysis = $propertyAnalysis;
    }

    /** @noinspection PhpDocMissingThrowsInspection */

    /**
     * 获取属性名称
     * @param ReflectionProperty $property 属性反射
     * @return string 合适的名称，要考虑别名、自动转换等
     */
    protected function getPropertyName(ReflectionProperty $property): string
    {
        $name = $property->name;
        $oas = $property->getAttributes(ObjectAttribute::class);
        $hasAlias = false;
        if ($oas) {
            /** @var ObjectAttribute $oa */
            $oa = $oas[0]->newInstance();
            if (strlen($oa->getAlias()) > 0) {
                $name = $oa->getAlias();
                $hasAlias = true;
            }
        }
        if (!$hasAlias && $this->propertyNameTrans) {
            $name = $this->propertyNameTrans->convert($name);
        }
        return $name;
    }

    /**
     * 判断一个属性是否可以忽略
     * @param ReflectionProperty $property 属性反射
     * @return bool 是否可忽略
     */
    protected function propertyIsIgnore(ReflectionProperty $property): bool
    {
        $oas = $property->getAttributes(ObjectAttribute::class);
        if ($oas) {
            /** @var ObjectAttribute $oa */
            $oa = $oas[0]->newInstance();
            $ignore = $oa->isIgnore();
        } else {
            $ignore = false;
        }
        return $ignore;
    }

    /**
     * 设置属性的值
     * @param PropertyResult $propertyResult 属性分析结果
     * @param mixed $value 输入的数据
     * @param object $object 要赋值的对象
     * @return void
     * @noinspection PhpDocMissingThrowsInspection
     */
    protected function setPropertyValue(PropertyResult $propertyResult, mixed $value, object $object): void
    {
        $property = $propertyResult->getProperty();
        $valueType = null;
        if ($this->tryCallSetter && $propertyResult->isHasSetter()) {
            $valueType = $propertyResult->getSetterTypeDescription();
        }
        if (!$valueType) {
            $valueType = $propertyResult->getTypeDescription();
        }
        try {
            $fixedValue = $this->createValue($value, $valueType, "{$property->class}::{$property->name} ");
        } catch (ConvertFailed $convertFailed) {
            if ($property->hasDefaultValue()) {
                return;
            } else {
                throw $convertFailed;
            }
        }
        if ($this->tryCallSetter && $propertyResult->isHasSetter()) {
            /** @noinspection PhpUnhandledExceptionInspection */
            $propertyResult->getSetter()->invoke($object, $fixedValue);
        } else {
            $allow = $property->isPublic();
            if (!$allow && FeatureJudge::needSetAccessible()) {
                // @codeCoverageIgnoreStart
                /** @noinspection PhpExpressionResultUnusedInspection */
                $property->setAccessible(true);
                // @codeCoverageIgnoreEnd
            }
            $property->setValue($object, $fixedValue);
            if (!$allow && FeatureJudge::needSetAccessible()) {
                // @codeCoverageIgnoreStart
                /** @noinspection PhpExpressionResultUnusedInspection */
                $property->setAccessible(false);
                // @codeCoverageIgnoreEnd
            }
        }
    }

    /**
     * 根据数据和数据描述生成数据
     * @param mixed $orgValue 原始数据
     * @param ?TypeDescription $typeDescription 类型描述
     * @param string $name 属性名称描述，用来做错误提示
     * @return mixed
     */
    protected function createValue(
        mixed $orgValue,
        ?TypeDescription $typeDescription,
        string $name = ""
    ): mixed {
        if (!$typeDescription || is_null($orgValue) && $typeDescription->isAllowNull()) {
            # 表示不受限
            return $orgValue;
        }
        $lastError = [];
        foreach ($typeDescription->getTypeDescriptions() as $oneMayTypeDesc) {
            try {
                $type = $oneMayTypeDesc->getPropertyType();
                if ($oneMayTypeDesc->isClass()) {
                    return $this->convertByRefAllowNull($orgValue, $type);
                }
                switch ($type) {
                    case BuildinType::CALLABLE:
                        return function () use ($orgValue) {
                            return $orgValue;
                        };
                    case BuildinType::ARRAY:
                        if (!is_array($orgValue)) {
                            throw new ConvertFailed("{$name}要求数组而传入非数组");
                        }
                        $elementType = $oneMayTypeDesc->getArrayElement();
                        if ($elementType) {
                            $list = [];
                            foreach ($orgValue as $idx => $item) {
                                $list[$idx] = $this->createValue($item, $elementType, $name);
                            }
                            if (!$oneMayTypeDesc->getIsMap()) {
                                $list = array_values($list);
                            }
                            return $list;
                        } else {
                            return $orgValue;
                        }
                    default:
                        if (isset(BuildinTypeHelper::VALUE_FORMAT[$type])) {
                            /** @var callable $formater */
                            $formater = BuildinTypeHelper::VALUE_FORMAT[$type];
                        } else {
                            $formater = null;
                        }
                        $checkFn = BuildinTypeHelper::CHECK_FUNC[$type] ?? null;
                        if (!$checkFn || $checkFn($orgValue)) {
                            return $formater ? $formater($orgValue) : $orgValue;
                        }
                        if (is_scalar($orgValue)) {
                            $fixed = $formater ? $formater($orgValue) : $orgValue;
                            if ($fixed == $orgValue) {
                                return $fixed;
                            }
                        }
                        throw new ConvertFailed("{$name}输入参数不是预期的 {$type}");
                }
            } catch (Throwable $throwable) {
                # 转失败了，换另一个
                if ($oneMayTypeDesc->isClass()) {
                    $key = $oneMayTypeDesc->getPropertyType()->getName();
                } else {
                    $key = $oneMayTypeDesc->getPropertyType();
                }
                $lastError[$key] = $throwable;
                continue;
            }
        }
        if (count($lastError) == 1) {
            $lastError = array_values($lastError)[0];
            throw new ConvertFailed($lastError->getMessage(), $lastError->getCode(), $lastError);
        } else {
            $text = "";
            foreach ($lastError as $key => $item) {
                $text .= "\n{$key} 转换失败 " . $item->getMessage();
            }
            throw new ConvertFailed("{$name}无法转换{$text}");
        }
    }

    /**
     * 通过类型名获取类型描述
     * @param string $name
     * @param ?ArrayDescAnno $arrayDesc
     * @return ?OneMayTypeDesc
     * @noinspection PhpDocMissingThrowsInspection
     */
    protected function getTypeDescByName(string $name, ArrayDescAnno $arrayDesc = null): ?OneMayTypeDesc
    {
        if (class_exists($name) || interface_exists($name)) {
            /** @noinspection PhpUnhandledExceptionInspection */
            return new OneMayTypeDesc(new ReflectionClass($name));
        } elseif (isset(BuildinTypeHelper::IS_SCALAR[$name])) {
            $one = new OneMayTypeDesc($name);
            if ($name != BuildinType::ARRAY) {
                return $one;
            }
            if (!$arrayDesc) {
                return $one;
            }
            $one->setIsMap($arrayDesc->isMap());
            $element = new TypeDescription();
            if ($arrayDesc->isElementIsClass()) {
                if (!class_exists($arrayDesc->getElementType())) {
                    throw new ConvertFailed("类 {$arrayDesc->getElementType()} 不存在，无法转换");
                }
                /** @noinspection PhpUnhandledExceptionInspection */
                $type = new OneMayTypeDesc(new ReflectionClass($arrayDesc->getElementType()));
                $element->addTypeDescription($type);
            } else {
                $element->addTypeDescription(new OneMayTypeDesc($arrayDesc->getElementType()));
            }
            if ($element->getTypeDescriptions()) {
                $one->setArrayElement($element);
            }
            return $one;
        } else {
            return null;
        }
    }

    /** @inheritDoc */
    public function convertByType(mixed $data, ReflectionType $reflectionType, array $annotations = []): mixed
    {
        return $this->createValue($data, $this->convertType2Desc($reflectionType, $annotations));
    }

    /**
     * 根据类型声明和默认值生成类型描述
     * @param ReflectionType $reflectionType
     * @param ReflectionAttribute[] $annotations 注解列表
     * @return ?TypeDescription
     * @noinspection PhpDocMissingThrowsInspection
     */
    protected function convertType2Desc(ReflectionType $reflectionType, array $annotations): ?TypeDescription
    {
        if ($reflectionType instanceof ReflectionUnionType) {
            $list = $reflectionType->getTypes();
        } elseif ($reflectionType instanceof ReflectionNamedType) {
            $list = [$reflectionType];
        } else {
            // @codeCoverageIgnoreStart
            throw new ConvertFailed("未知的类型描述类 " . get_class($reflectionType));
            // @codeCoverageIgnoreEnd
        }
        /** @var ArrayDescAnno $arrayDesc */
        $arrayDesc = null;
        foreach ($annotations as $annotation) {
            if ($annotation->getName() == ArrayDescAnno::class) {
                $arrayDesc = $annotation->newInstance();
                break;
            }
        }
        $typeResult = new TypeDescription($reflectionType, allowNull: $reflectionType->allowsNull());
        /** @var ReflectionNamedType $namedType */
        foreach ($list as $namedType) {
            $one = $this->getTypeDescByName($namedType->getName(), $arrayDesc);
            if ($one) {
                $typeResult->addTypeDescription($one);
            }
        }
        return $typeResult->getTypeDescriptions() ? $typeResult : null;
    }
}

# end of file
